/**
 *  @file FileSys.cpp
 */
#if defined(WIN32)
#   include <windows.h>
#elif defined(linux)
#   include <stdio.h>
#   include <stdlib.h>
#   include <sys/stat.h>
#   include <sys/types.h>
#   include <dirent.h>
#   include <unistd.h>
#   include <errno.h>
#   include <string.h>
#endif
#include "../common/CppflibMain.h"
#include "../common/CppMemPool.h"
#include "../common/ObjPtrT.h"
#include "FileSys.h"
#include "../io/FileStream.h"


using namespace cppflib::collections;
using namespace cppflib::util;

namespace cppflib
{

namespace os
{

/**
 *  Delete a file
 */
_DLLAPI bool DelFile(pcwstr_t filePath)
{

#if defined(WIN32)
   return ::DeleteFile(filePath) ? true : false;
#elif defined(linux)
   ObjPtrT<char> fileName(CFString::ConvertToUtf8String(filePath));
   return (unlink(fileName) == 0);
#endif

}

/**
 *  Copy file
 */
_DLLAPI bool CpFile(pcwstr_t orgFile, pcwstr_t newFile, bool failIfExists)
{
#if defined(WIN32)
   return (::CopyFile(orgFile, newFile, failIfExists ? TRUE : FALSE) != 0);
#elif defined(linux)
   if (failIfExists) {
      if (FileExists(newFile))
         return false;
   }

   // Linux has no 'CopyFile' equivalant function, so copy file manually here
   ObjPtrT<char> newFileName(CFString::ConvertToUtf8String(newFile));
   ObjPtrT<char> orgFileName(CFString::ConvertToUtf8String(orgFile));
   FILE *srcFp = NULL, *destFp = NULL;
   bool isOK = false;

   do {
      if ((srcFp = fopen(orgFileName, "rb")) == NULL) break;
      if ((destFp = fopen(newFileName, "wb")) == NULL) break;

      const int BUFLEN = 1024;
      char buf[BUFLEN];
      size_t nrBytes;
      while (!feof(srcFp)) {
         if ((nrBytes = fread(buf, sizeof(char), BUFLEN, srcFp)) <= 0) break;
         if (fwrite(buf, sizeof(char), nrBytes, destFp) < nrBytes) break;
      }

      isOK = true;
   } while(false);

   if (srcFp) fclose(srcFp);
   if (destFp) fclose(destFp);
   
   return isOK;
#endif
}

/**
 *  Move / rename a file
 */
_DLLAPI bool MvFile(pcwstr_t orgFile, pcwstr_t newFile, bool failIfExists)
{
#if defined(WIN32)
   return (::MoveFileEx(orgFile, newFile, failIfExists ? 0 : MOVEFILE_REPLACE_EXISTING) != 0);
#elif defined(linux)
   if (failIfExists) {
      if (FileExists(newFile))
         return false;
   }

   ObjPtrT<char> newFileName(CFString::ConvertToUtf8String(newFile));
   ObjPtrT<char> orgFileName(CFString::ConvertToUtf8String(orgFile));
   return (rename(orgFileName, newFileName) == 0);
#endif
}


PRIVATE bool __MakeOneDirectory(pcwstr_t pathName)
{
#if defined(WIN32)
      return (::CreateDirectory(pathName, NULL) != 0);
#elif defined(linux)
      ObjPtrT<char> dirName(CFString::ConvertToUtf8String(pathName));
      return (mkdir(dirName, 0777) == 0); // the final mode will be (mode & ~umask & 0777)
#endif
}

/**
 *  Make directory
 *
 *  @param [in] createIntermediateDirs -- true to create the intermediate directories, too
 */
_DLLAPI bool MakeDirectory(pcwstr_t pathName, bool createIntermediateDirs)
{

   if (!createIntermediateDirs) {
      return __MakeOneDirectory(pathName);
   }
   
   // create intermediate directories as well
   CFString orgPath(pathName);
   pcwstr_t sepToFind = _S("/\\");
   int startPos = 0;
   
   while (startPos < orgPath.GetLength()) {
      int pos = orgPath.FindOneOf(sepToFind, startPos);
      pos = (pos < 0) ? orgPath.GetLength() : pos;

      if (pos == startPos) {
         // two consecutive slashes encountered
         ++startPos;
      }
      else {
         CFString tempDir = orgPath.Left(pos);
         if (!FileExists(static_cast<pcwstr_t>(tempDir))) {
            if (!__MakeOneDirectory(static_cast<pcwstr_t>(tempDir)))
               return false;
         }
         startPos = pos + 1;
      }
   }

   return true;
}

/**
 *  Remove an empty directory
 */
_DLLAPI bool DelDirectory(pcwstr_t pathName)
{
#if defined(WIN32)
   return (::RemoveDirectory(pathName) != 0);
#elif defined(linux)
   ObjPtrT<char> dirName(CFString::ConvertToUtf8String(pathName));
   return (rmdir(dirName) == 0);
#endif
}

/**
 *  Get the current directory
 *  @return absolute path of current directory
 */
_DLLAPI void GetCurDirectory(CFString &path)
{
#if defined(WIN32)
   // get size first
   int sizeRequired = static_cast<int>(::GetCurrentDirectory(0, NULL)) + 1;
   ObjPtrT<wc_t> pCurDir(static_cast<wc_t*>(::MRP_Calloc(sizeRequired, sizeof(wc_t))));
   // get real directory string
   ::GetCurrentDirectory(sizeRequired, pCurDir);
   path = static_cast<pcwstr_t>(pCurDir);
#elif defined(linux)
   int bufSize = 256;
   while (true) {
      ObjPtrT<char> pCurDir(static_cast<char*>(::MRP_Calloc(bufSize, sizeof(char))));
      if (getcwd(pCurDir, bufSize)) {
         path = static_cast<const char*>(pCurDir);
         break;
      }
      else {
         // error
         if (cppflib::GetLastError() == ERANGE) {
            bufSize = bufSize * 2;  // increase buffer size
         }
         else {
            path = _S("");
            break;   // bread for other errors
         }  
      }
   }
#endif
}

/**
 *  Change current directory 
 *  @param [in] pathName -- absolute or relative path to a directory
 */
_DLLAPI bool ChangeDirectory(pcwstr_t pathName)
{
#if defined(WIN32)
   return (::SetCurrentDirectory(pathName) != 0);
#elif defined(linux)
   ObjPtrT<char> dirName(CFString::ConvertToUtf8String(pathName));
   return (chdir(dirName) == 0);
#endif
}


/**
 *  Get file size
 */
_DLLAPI u64_t GetFileSize(pcwstr_t filePath)
{
   io::FileStream f(filePath, io::FileStream::FA_READONLY);
   u64_t s = f.GetFileSize();
   f.Close();
   return s;
}

/**
 *  Check if a file/dir exists
 */
_DLLAPI bool FileExists(pcwstr_t filePath)
{
#if defined(WIN32)
   return (::GetFileAttributes(filePath) != INVALID_FILE_ATTRIBUTES);
#elif defined(linux)
   ObjPtrT<char> fileName(CFString::ConvertToUtf8String(filePath));
   struct stat st;
   return (stat(fileName, &st) == 0);
#endif
}

//////////////////////// File class /////////////////////////////

#if defined(WIN32)

const pcwstr_t File::PathSeparator = _S(";");
const wc_t File::PathSeparatorChar = _S(';');
const pcwstr_t File::DirSeparator = _S("\\");
const wc_t File::DirSeparatorChar = _S('\\');

File::File(void)
{
}

PRIVATE void __PostProcessFilePath(CFString &fullFilePath)
{
   // make sure it isn't "c:\", "D:\", ...
   if (!(fullFilePath.GetLength() == 3 && fullFilePath.EndsWith(_S(":\\")))) { 
      if (fullFilePath.EndsWith(File::DirSeparatorChar))
         fullFilePath.TrimRight(File::DirSeparatorChar); // trim the separator except root
   }
}

File::File(pcwstr_t fullPath)
{
   this->fullFilePath = fullPath;
   __PostProcessFilePath(this->fullFilePath);
}

File::File(const char * fullPath)
{
   this->fullFilePath = fullPath;
   __PostProcessFilePath(this->fullFilePath);
}

File::~File(void)
{
}

bool File::CanExecute()
{
   u32_t binaryType;
   return (::GetBinaryType(fullFilePath, reinterpret_cast<LPDWORD>(&binaryType)) != 0);
}

bool File::CanRead()
{
   // try to open it
   HANDLE hFile = ::CreateFile(fullFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 
                               FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS, NULL);
   if (hFile == INVALID_HANDLE_VALUE) 
      return false;

   ::CloseHandle(hFile);
   return true;
}

bool File::CanWrite()
{
   u32_t attr = ::GetFileAttributes(fullFilePath);
   return (attr != INVALID_FILE_ATTRIBUTES) && ((attr & FILE_ATTRIBUTE_READONLY) == 0);
}

bool File::Exists()
{
   return FileExists(fullFilePath);
}

bool File::IsDir()
{
   u32_t attr = ::GetFileAttributes(fullFilePath);
   return (attr != INVALID_FILE_ATTRIBUTES) && ((attr & FILE_ATTRIBUTE_DIRECTORY) != 0);
}

bool File::IsFile()
{
   u32_t attr = ::GetFileAttributes(fullFilePath);
   return (attr != INVALID_FILE_ATTRIBUTES) && ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0);
}

bool File::IsHidden()
{
   u32_t attr = ::GetFileAttributes(fullFilePath);
   return (attr != INVALID_FILE_ATTRIBUTES) && ((attr & FILE_ATTRIBUTE_HIDDEN) != 0);
}

/**
 *  @param [in] isGetCreateTime -- true to get create time
 *                                 false to get last modified time
 */
PRIVATE DateTime __GetFileTime(pcwstr_t filePath, bool isGetCreateTime)
{
   WIN32_FILE_ATTRIBUTE_DATA fileData;
   if (!::GetFileAttributesEx(filePath, GetFileExInfoStandard, &fileData))
      return DateTime(0);

   SYSTEMTIME sysTime;
   FILETIME *fileTime = isGetCreateTime ? &fileData.ftCreationTime : &fileData.ftLastWriteTime;
   ::FileTimeToSystemTime(fileTime, &sysTime);

   return DateTime(sysTime.wYear, sysTime.wMonth, sysTime.wDay,
                   sysTime.wHour, sysTime.wMinute, sysTime.wSecond,
                   sysTime.wMilliseconds, DateTime::DTK_UTC);
} 

DateTime File::GetCreationTime()
{
   return __GetFileTime(fullFilePath, true);
}

DateTime File::GetLastModifiedTime()
{
   return __GetFileTime(fullFilePath, false);
}

u64_t File::GetSize()
{
   return GetFileSize(fullFilePath);
}

void File::GetAbsolutePath(CFString &path)
{
   path = this->fullFilePath;
}

/**
 *  Get only the file/dir name
 */
void File::GetName(CFString &fileName)
{
   if (fullFilePath.EndsWith(File::DirSeparatorChar)) { // only root ends with dir separator
      fileName = fullFilePath;  // return itself
   }
   else {
      int pos = fullFilePath.ReverseFind(File::DirSeparatorChar);
      pos = (pos < 0) ? 0 : pos;
      fileName = fullFilePath.Right(fullFilePath.GetLength() - (pos + 1));
   }
}

/**
 *  Get parent directory path
 */
void File::GetParent(CFString &parentPath)
{
   if (fullFilePath.EndsWith(File::DirSeparatorChar)) { // only root ends with dir separator
      parentPath = fullFilePath;  // return itself
   }
   else {
      int pos = fullFilePath.ReverseFind(File::DirSeparatorChar);
      pos = (pos < 0) ? 0 : pos;
      parentPath = fullFilePath.Left(pos);
   }
}

PRIVATE HANDLE __GetFileHandle(pcwstr_t curDir)
{
   CFString dir;
   dir.Format(_S("%s\\*"), curDir);

   WIN32_FIND_DATA findFileData;
   return ::FindFirstFile(dir, &findFileData);
}

/**
 *  Get list of file/dir names inside this directory
 *  @return NULL if it is not a directory or non-readable
 */
ListCppObjT<CFString> * File::GetChildFileNames()
{
   HANDLE hFind = __GetFileHandle(fullFilePath);
   if (hFind == INVALID_HANDLE_VALUE)
      return NULL;

   WIN32_FIND_DATA findFileData;
   ListCppObjT<CFString> *fileList = new ListCppObjT<CFString>();
   while(::FindNextFile(hFind, &findFileData)) {
      if (CFString::WStrcmp(findFileData.cFileName, _S("..")) != 0)
         fileList->AddTail(new CFString(findFileData.cFileName));
   }
   ::FindClose(hFind);

   return fileList;
}

/**
 *  Get list of files inside this directory
 *  @return NULL if it is not a directory or non-readable
 */
ListCppObjT<File> * File::GetChildFiles()
{
   HANDLE hFind = __GetFileHandle(fullFilePath);
   if (hFind == INVALID_HANDLE_VALUE)
      return NULL;

   WIN32_FIND_DATA findFileData;
   CFString childPath;
   pcwstr_t formatStr = fullFilePath.EndsWith(File::DirSeparatorChar) ? _S("%s%s") : _S("%s\\%s");
   ListCppObjT<File> *fileList = new ListCppObjT<File>();

   while(::FindNextFile(hFind, &findFileData)) {
      if (CFString::WStrcmp(findFileData.cFileName, _S("..")) != 0) {
         childPath.Format(formatStr, static_cast<pcwstr_t>(fullFilePath), findFileData.cFileName);
         fileList->AddTail(File::GetInstance(childPath));
      }
   }
   ::FindClose(hFind);

   return fileList;
}

/**
 *  @return an instance of File representing a file/dir
 *  @return NULL if the file/dir does not exist
 */
File * File::GetInstance(pcwstr_t filePath)
{
   if (!FileExists(filePath))
      return NULL;

   u32_t bufLen = ::GetFullPathName(filePath, 0, NULL, NULL);
   ObjPtrT<wc_t> tempPath(static_cast<wc_t*>(::MRP_Calloc(bufLen, sizeof(wc_t))));
   ::GetFullPathName(filePath, bufLen, tempPath, NULL);

   File *pFile = new File(tempPath);
   return pFile;
}

#elif defined(linux)

const pcwstr_t File::PathSeparator = _S(":");
const wc_t File::PathSeparatorChar = _S(':');
const pcwstr_t File::DirSeparator = _S("/");
const wc_t File::DirSeparatorChar = _S('/');

PRIVATE void __PostProcessFilePath(CFString &fullFilePath)
{
   if (fullFilePath != _S("/")) { 
      if (fullFilePath.EndsWith(File::DirSeparatorChar))
         fullFilePath.TrimRight(File::DirSeparatorChar); // trim the separator except root
   }
}

File::File(void)
{
}

File::File(pcwstr_t fullPath)
{
   this->fullFilePath = fullPath;
   __PostProcessFilePath(this->fullFilePath);
}

File::File(const char * fullPath)
{
   this->fullFilePath = fullPath;
   __PostProcessFilePath(this->fullFilePath);
}

File::~File(void)
{
}

PRIVATE bool __GetFileMode(CFString &filePath, mode_t &m)
{
   ObjPtrT<char> fileName(filePath.ToUtf8String());
   m = 0;

   struct stat st;
   if (stat(fileName, &st) != 0)
      return false;

   m = st.st_mode;
   return true;
}

bool File::CanExecute()
{
   mode_t mode;
   if (!__GetFileMode(fullFilePath, mode))
      return false;
   
   return ((mode & S_IXOTH) != 0) ||
          ((mode & S_IXGRP) != 0) ||
          ((mode & S_IXUSR) != 0);
}

bool File::CanRead()
{
   mode_t mode;
   if (!__GetFileMode(fullFilePath, mode))
      return false;
   
   return ((mode & S_IROTH) != 0) ||
          ((mode & S_IRGRP) != 0) ||
          ((mode & S_IRUSR) != 0);
}

bool File::CanWrite()
{
   mode_t mode;
   if (!__GetFileMode(fullFilePath, mode))
      return false;
   
   return ((mode & S_IWOTH) != 0) ||
          ((mode & S_IWGRP) != 0) ||
          ((mode & S_IWUSR) != 0);
}

bool File::Exists()
{
   return FileExists(fullFilePath);
}

bool File::IsDir()
{
   mode_t mode;
   if (!__GetFileMode(fullFilePath, mode))
      return false;

   return S_ISDIR(mode);
}

bool File::IsFile()
{
   mode_t mode;
   if (!__GetFileMode(fullFilePath, mode))
      return false;

   return !S_ISDIR(mode);
}

bool File::IsHidden()
{
   return fullFilePath.StartsWith(_S("."));
}

/**
 *  Linux does not store the 'creation time' of a file until 'ext4'
 */
DateTime File::GetCreationTime()
{
   return GetLastModifiedTime();
}

DateTime File::GetLastModifiedTime()
{
   ObjPtrT<char> fileName(fullFilePath.ToUtf8String());
   struct stat st;
   if (stat(fileName, &st) != 0)
      return DateTime(0);

   return DateTime::GetDateTimeFromEpoch(st.st_mtime);
}


u64_t File::GetSize()
{
   return GetFileSize(fullFilePath);
}

void File::GetAbsolutePath(CFString &path)
{
   path = this->fullFilePath;
}

void File::GetName(CFString &fileName)
{
   if (fullFilePath.EndsWith(File::DirSeparatorChar)) { // only root ends with dir separator
      fileName = fullFilePath;  // return itself
   }
   else {
      int pos = fullFilePath.ReverseFind(File::DirSeparatorChar);
      pos = (pos < 0) ? 0 : pos;
      fileName = fullFilePath.Right(fullFilePath.GetLength() - (pos + 1));
   }
}

void File::GetParent(CFString &parentPath)
{
   if (fullFilePath.EndsWith(File::DirSeparatorChar)) { // only root ends with dir separator
      parentPath = fullFilePath;  // return itself
   }
   else {
      int pos = fullFilePath.ReverseFind(File::DirSeparatorChar);
      pos = (pos < 0) ? 0 : pos;
      parentPath = fullFilePath.Left(pos);
   }
}

ListCppObjT<CFString> * File::GetChildFileNames()
{
   ObjPtrT<char> fileName(fullFilePath.ToUtf8String());
   DIR *pDir = opendir(fileName);
   if (!pDir)
      return NULL;

   struct dirent *pDirEnt;
   ListCppObjT<CFString> *fileList = new ListCppObjT<CFString>();

   while (pDirEnt = readdir(pDir)) {
      if (strcmp(pDirEnt->d_name, ".") != 0 && strcmp(pDirEnt->d_name, "..") != 0) {
         fileList->AddTail(new CFString(pDirEnt->d_name));
      }
   }

   closedir(pDir);
   return fileList;
}

ListCppObjT<File> * File::GetChildFiles()
{
   ObjPtrT<char> fileName(fullFilePath.ToUtf8String());
   DIR *pDir = opendir(fileName);
   if (!pDir)
      return NULL;

   struct dirent *pDirEnt;
   CFString childPath;
   // filename is in UTF-8
   pcwstr_t formatStr = fullFilePath.EndsWith(File::DirSeparatorChar) ? _S("%s%S") : _S("%s/%S");
   ListCppObjT<File> *fileList = new ListCppObjT<File>();

   while (pDirEnt = readdir(pDir)) {
      if (strcmp(pDirEnt->d_name, ".") != 0 && strcmp(pDirEnt->d_name, "..") != 0) {
         childPath.Format(formatStr, static_cast<pcwstr_t>(fullFilePath), pDirEnt->d_name);
         fileList->AddTail(File::GetInstance(childPath));
      }
   }

   closedir(pDir);
   return fileList;
}
 
/**
 *  @return an instance of File representing a file/dir
 *  @return NULL if the file/dir does not exist
 */
File * File::GetInstance(pcwstr_t filePath)
{
   if (!FileExists(filePath))
      return NULL;

   ObjPtrT<char> fileName(CFString::ConvertToUtf8String(filePath));
   char * fullRealPath = realpath(fileName, NULL);
   
   File *pFile = new File(fullRealPath);
   free(fullRealPath);
   return pFile;
}

#endif


} // end of namespace os

} // end of namespace cppflib

